package checkers3d.presentation;

import java.awt.Point;
import java.awt.Rectangle;
import java.util.Random;

/**
 * A simple and experimental particle system. Managings the creation, simulation,
 * and destruction, of a given number of graphical components under prescribed
 * conditions.
 * 
 * @author Ruben Acuna
 */
public class ParticleSystem
{
    /**
     * A pad area around the window in which particles will not be culled even
     * if they are not visible.
     */
    private final int CULL_PAD = 48;

    /**
     * The relative path to the location where particle graphics are stored.
     */
    public static final String LOCATION_PARTICLES = ".//particles/";

    /**
     * The GUIContainer this object should display in.
     */
    private GUIContainer parentContainer;

    /**
     * A rectangle in screen space that the particles must spawn within.
     */
    private Rectangle regionEmitter;

    /**
     * The maximum number of particles in this system.
     */
    private int particleCount;

    /**
     * The time (in milliseconds) between the addition of new particles into the
     * system.
     */
    private long particleWait;

    /**
     * The an array of IRenderResources for the display of particles.
     */
    private IRenderResource[] particleResources;

    /**
     * An array containing the particle in this system.
     */
    private Particle[] particles;

    /**
     * A direction vector for a possible velocity.
     */
    private Vector velocityDirection;
    
    /**
     * A magnitude for a possible starting velocity.
     */
    private float velocityMagnitude;

    /**
     * The acceleration vector that particles in this system are subjected to.
     */
    private Vector gfAcceleration;

    /**
     * The angular momentum of particles when created.
     */
    private float angularMomentum;

    /**
     * The change in a particle's draw scale over time.
     */
    private float scaleGrowth;

    /**
     * How long the particle should live in millisenconds.
     */
    private long lifeSpan;

    /**
     * The time at which a particle was last created.
     */
    private long lastCreationTime;

    /**
     * Random number generator.
     */
    private Random generator = new Random();

    /**
     * Flag indicating if particles in this system respawn.
     */
    private boolean continuous;

    /**
     * Class constructor for setting up a particle system with a given set of
     * behaviors.
     *
     * @param regionEmitter A rectangle in screen space that the particles must
     *                      spawn within.
     * @param velocityDirection A direction vector for a possible velocity.
     * @param velocityMagnitude A magnitude for a possible starting velocity.
     *                          Will be multiplied by a random floating point
     *                          value scalar
     * @param gfAcceleration The acceleration vector that particles in this
     *                       system are subjected to.
     * @param angularMomentum The angular momentum of particles when created in
     *                        radians per second.
     * @param scaleGrowth   The change in scale of particles in this system per
     *                      second.
     * @param lifeScap      How long the particle should live in seconds. Zero
     *                      to disable.
     * @param particleCount The maximum number of particles in this system.
     * @param particleWait  The time (in milliseconds) between the addition of
     *                      new particles into the system.
     * @param particleResource The IRenderResource for diplay of particles.
     * @param parentContainer The GUIContainer this object should display in.
     */
    public ParticleSystem(Rectangle regionEmitter, Vector velocityDirection,
                          float velocityMagnitude, Vector gfAcceleration, 
                          float angularMomentum, float particleGrowth, int lifeSpan,
                          int particleCount, long particleWait,
                          boolean continuous, GUIContainer parentContainer,
                          IRenderResource ... particleResources)
    {
        this.regionEmitter = regionEmitter;
        this.velocityDirection = velocityDirection.getNormalizedCopy();
        this.velocityMagnitude = velocityMagnitude;
        this.gfAcceleration = gfAcceleration.getScaledCopy(1 / 1000000f);//converts from m^2 to ms^2
        this.angularMomentum = angularMomentum / 1000f;
        this.scaleGrowth = particleGrowth / 1000f;
        this.lifeSpan = lifeSpan * 1000;
        this.particleCount = particleCount;
        this.particleWait = particleWait;
        this.continuous = continuous;
        this.parentContainer = parentContainer;
        this.particleResources = particleResources;

        particles = new Particle[this.particleCount];

        for(int i = 0; i < particles.length; i++)
        {
            particles[i] = new Particle(new GUIPrimitive(null, new Point()));
            parentContainer.add(particles[i].getDrawable());
        }

        lastCreationTime = System.currentTimeMillis() - particleWait;
    }

    /**
     * Creates new particles if any were culled.
     */
    public void refreshUnusedParticles()
    {
        for(int i = 0; i < particles.length; i++)
        {
            long currentTime = System.currentTimeMillis();

            //only create a new particle if there is a free particle slot and
            //enough time has elapsed.
            if(!particles[i].isAlive() && (((currentTime - lastCreationTime) > particleWait) || lastCreationTime == 0))
            {
                createParticle(i);
                lastCreationTime = currentTime;
            }
        }
    }

    /**
     * Forces the creation of all particles this system can produce.
     */
    public void createAllParticles()
    {
        for(int i = 0; i < particleResources.length; i++)
            particleResources[i].reset();
        
        for(int i = 0; i < particles.length; i++)
        {
                createParticle(i);
        }
        
        lastCreationTime = System.currentTimeMillis();
    }

    private void createParticle(int i)
    {
        particles[i].setAlive(true);
        particles[i].resetProperties();

        IRenderResource resource = particleResources[generator.nextInt(particleResources.length)].clone();
        int emitterPositionX = regionEmitter.x + generator.nextInt(regionEmitter.width) - resource.getWidth() / 2;
        int emitterPositionY = regionEmitter.y + generator.nextInt(regionEmitter.height) - resource.getHeight() / 2;
        particles[i].position.x = emitterPositionX;
        particles[i].position.y = emitterPositionY;

        float mag = velocityMagnitude * ((generator.nextFloat() + 2) / 3) / 1000f;
        particles[i].velocity = velocityDirection.getScaledCopy(mag);

        particles[i].acceleration = gfAcceleration.clone();
        particles[i].angularMomentum = angularMomentum;
        particles[i].scaleGrowth = scaleGrowth;
        particles[i].lifeSpan = lifeSpan;

        particles[i].getDrawable().setRenderResource(resource);
        particles[i].getDrawable().getDrawPosition().setLocation(particles[i].position.x, particles[i].position.y);
    }

    /**
     * Checks for particles that are too old and flags them for reuse. Particles
     * with lifespan of 0 are skipped.
     */
    private void cullLifetime()
    {
        for(int i = 0; i < particles.length; i++)
        {
            if(particles[i].isAlive())
            {
                if(!(particles[i].lifeSpan == 0) && particles[i].lifeTime > particles[i].lifeSpan)
                    destroyParticle(i);                    
            }
        }
    }

    /**
     * Checks for particles that are no long visible and flags them for reuse.
     */
    private void cullVisibility()
    {
        for(int i = 0; i < particles.length; i++)
        {
            if(particles[i].isAlive())
            {
                int xRight = particles[i].getDrawable().getDrawPosition().x
                           + particles[i].getDrawable().getRenderResource().getWidth();
                int xLeft = particles[i].getDrawable().getDrawPosition().x;

                int yTop = particles[i].getDrawable().getDrawPosition().y
                         + particles[i].getDrawable().getRenderResource().getHeight();
                int yBottom = particles[i].getDrawable().getDrawPosition().y;

                if(xRight < 0 - CULL_PAD || xLeft > parentContainer.getSize().x + CULL_PAD ||
                   yTop < 0 - CULL_PAD || yBottom > parentContainer.getSize().y + CULL_PAD)
                {
                    //the particle is outside of the render window and cull pad.
                    destroyParticle(i);
                }
            }
        }
    }

    /**
     * Disables a particle by setting alive to false and removing it's
     * RenderResource and position.
     *
     * @param i Index of particle to destroy.
     */
    private void destroyParticle(int i)
    {
        particles[i].getDrawable().setRenderResource(null);
        particles[i].getDrawable().setDrawPosition(new Point());
        particles[i].setAlive(false);
    }

    /**
     * Sets this particle system to be continuous
     *
     * @param continuous Flag indicating if system should be continuous
     */
    public void setContinuous(boolean continuous)
    {
        this.continuous = continuous;
    }

    /**
     * Informs the GUIContainer that a given amount of time in milliseconds has
     * elapsed. Used for animation and time events.
     *
     * @param ms Milliseconds elapsed.
     */
    public void tick(int ms)
    {
        for(int i = 0; i < particles.length; i++)
        {
            if(particles[i].isAlive())
            {
                particles[i].lifeTime += ms;

                particles[i].position.translate(particles[i].velocity.getScaledCopy(ms));
                particles[i].velocity.translate(particles[i].acceleration.getScaledCopy(ms));
                particles[i].rotation += (particles[i].angularMomentum * ms);
                particles[i].rotation = (float)(particles[i].rotation % (Math.PI * 2));

                particles[i].scale += particles[i].scale * particles[i].scaleGrowth * ms;

                //update the particle's IDrawable variables from the particle itself.
                particles[i].getDrawable().getDrawPosition().setLocation((int)particles[i].position.x,(int)particles[i].position.y);
                
                //TODO: scaling and rotation are only support by RenderResourceBitmap.
                if(particles[i].getDrawable().getRenderResource() instanceof RenderResourceBitmap)
                {
                    RenderResourceBitmap resource = (RenderResourceBitmap)particles[i].getDrawable().getRenderResource();

                    resource.setRotation(particles[i].rotation);
                    resource.setScale(particles[i].scale);
                }

                particles[i].getDrawable().getRenderResource().tick(ms);
            }
        }

        cullVisibility();
        cullLifetime();
        
        if(continuous)
            refreshUnusedParticles();
    }

    /**
     * Sets the current array of RenderResources used to display particles
     *
     * @param resources Array of RenderResources.
     */
    public void setRenderResources(IRenderResource ... resources)
    {
        this.particleResources = resources;
    }

    /**
     * Sets the current region that particles are created within.
     *
     * @param regionEmitter Region that particles are created within.
     */
    public void setEmitterRegion(Rectangle regionEmitter)
    {
        this.regionEmitter = regionEmitter;
    }

    /**
     * Returns a string representation of this object containing it's class name.
     *
     * @return String representation.
     */
    @Override
    public String toString()
    {
        return getClass().getName();
    }

    /**
     * A class representing a visible 'particle' object with velocity and
     * acceleration.
     */
    private class Particle
    {
        /**
         * The associated GUIComponent.
         */
        private IDrawable drawable;

        /**
         * Indicates if particle is being used.
         */
        private boolean alive;

        /**
         * The position vector of the particle. Units are meters.
         */
        public Vector position;

        /**
         * The velocity vector of the particle. Units are meters per milliseconds.
         */
        public Vector velocity;

        /**
         * Rotation of particle in radians.
         */
        public float rotation;

        /**
         * The current angular momentum of the particle. Units are meters per milliseconds.
         */
        public float angularMomentum;

        /**
         * The intended draw scale of this particle.
         */
        public float scale;

        /**
         * The change in the draw scale over time.
         */
        public float scaleGrowth;

        /**
         * The number of milliseconds this particle has lived.
         */
        public long lifeTime;

        /**
         * The total number of milliseconds this particle can live.
         */
        public long lifeSpan;

        /**
         * The acceleration vector of the particle. Units are meters per
         * milliseconds squared.
         */
        public Vector acceleration;

        public Particle(IDrawable particle)
        {
            this.drawable = particle;

            resetProperties();

            setAlive(false);
        }

        /**
         * Resets the properties of the particle by setting position, velocity, 
         * acceleration, rotation, angular momentum, scale, and scale growth to
         * zero.
         */
        public void resetProperties()
        {
            position = new Vector();
            velocity = new Vector();
            acceleration = new Vector();
            rotation = 0.0f;
            angularMomentum = 0.0f;
            scale = 1.0f;
            scaleGrowth = 0.0f;
            lifeTime = 0;
            lifeSpan = 0;
        }

        /**
         * Returns the GUIComponent associated with this particle.
         *
         * @return The associated GUIComponent.
         */
        public IDrawable getDrawable()
        {
            return drawable;
        }

        /**
         * Sets the GUIComponent associated with this particle.
         * 
         * @param particle The associated GUIComponent.
         */
        public void setDrawable(IDrawable particle)
        {
            this.drawable = particle;
        }

        /**
         * Returns value indicating if particle is being used.
         *
         * @return Indicates if particle is being used.
         */
        public boolean isAlive()
        {
            return alive;
        }

        /**
         * Sets a value indicating if particle is being used.
         *
         * @param alive Indicates if particle is being used.
         */
        public void setAlive(boolean alive)
        {
            this.alive = alive;
        }
    }
}